package com.google.jessewilson.codesearch.combining;

import ca.odell.glazedlists.CompositeList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.UniqueList;
import com.google.jessewilson.codesearch.api.ContentRoot;
import com.google.jessewilson.codesearch.api.Line;
import com.google.jessewilson.codesearch.api.QueryResponse;
import com.google.jessewilson.codesearch.api.ResultsPopulator;
import com.google.jessewilson.codesearch.util.Resource;
import com.google.jessewilson.codesearch.util.IntersectionList;
import com.google.jessewilson.codesearch.util.Progress;

import java.util.Comparator;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

/**
 * @author jessewilson
 */
public interface ResultsCombiner {

  QueryResponse combine(QueryResponse a, QueryResponse b);

  static Comparator<Line> fileComparator = new Comparator<Line>() {
    private String getContentRoot(Line line) {
      ContentRoot contentRoot = line.getCodeFile().getContentRoot();
      return contentRoot != null
          ? (contentRoot.getContentPath() + "/" + contentRoot.getContentPath())
          : "";
    }

    private String getFileString(Line line) {
      return line.getCodeFile().getPath().toString();
    }

    public int compare(Line a, Line b) {
      int result = 0;

      // compare by content root
      result = getContentRoot(a).compareTo(getContentRoot(b));
      if (result != 0) {
        return result;
      }

      // compare by path
      result = getFileString(a).compareTo(getFileString(b));
      if (result != 0) {
        return result;
      }

      return result;
    }
  };

  static Comparator<Line> lineComparator = new Comparator<Line>() {

    public int compare(Line a, Line b) {
      int result = 0;

      // compare by file
      result = fileComparator.compare(a, b);
      if (result != 0) {
        return result;
      }

      // compare by line number
      result = a.getLineNumber() - b.getLineNumber();
      if (result != 0) {
        return result;
      }

      return 0;
    }
  };

  public static ResultsCombiner intersection = new ResultsCombiner() {
    public QueryResponse combine(QueryResponse a, QueryResponse b) {
      return new IntersectionQueryResponse(a, b);
    }
  };

  public static ResultsCombiner union = new ResultsCombiner() {
    public QueryResponse combine(final QueryResponse a, final QueryResponse b) {
      return new UnionQueryResponse(a, b);
    }
  };

  /**
   * A {@link QueryResponse} that is the intersection of other {@link QueryResponse}.
   */
  public static class IntersectionQueryResponse implements QueryResponse, Resource.User {
    private final Handle handle = new Handle(this);

    // keep these around so they can be disposed
    private final QueryResponse resultsA;
    private final QueryResponse resultsB;
    private final IntersectionList<Line> aMatches;
    private final IntersectionList<Line> bMatches;
    private final CompositeList<Line> compositeList;
    private final UniqueList<Line> intersection;
    private final Progress progress = new Progress();
    private final ResultsPopulator resultsPopulator;

    private final String shortDescription;

    public IntersectionQueryResponse(QueryResponse resultsA, QueryResponse resultsB) {
      this.resultsA = resultsA;
      this.resultsB = resultsB;

      // register myself user for resultsA and resultsB
      resultsA.handle().reserve(this);
      resultsB.handle().reserve(this);

      // intersect the results
      EventList<Line> aLines = resultsA.getLines();
      EventList<Line> bLines = resultsB.getLines();
      aMatches = new IntersectionList<Line>(aLines, fileComparator);
      aMatches.addIntersection(bLines);
      bMatches = new IntersectionList<Line>(bLines, fileComparator);
      bMatches.addIntersection(aLines);
      compositeList = new CompositeList<Line>(aLines.getPublisher(), aLines.getReadWriteLock());
      compositeList.addMemberList(aMatches);
      compositeList.addMemberList(bMatches);
      intersection = new UniqueList<Line>(compositeList, lineComparator);

      shortDescription = resultsA.getShortDescription() + "|" + resultsB.getShortDescription();

      resultsPopulator = new CombinedResultsPopulator(
          Arrays.asList(resultsA.getResultsPopulator(), resultsB.getResultsPopulator()));
    }

    public EventList<Line> getLines() {
      return intersection;
    }

    public String getShortDescription() {
      return shortDescription;
    }

    public ResultsPopulator getResultsPopulator() {
      return resultsPopulator;
    }

    public Handle handle() {
      return handle;
    }

    public void dispose() {
      intersection.dispose();
      compositeList.dispose();
      aMatches.dispose();
      bMatches.dispose();
      resultsA.handle().releaseAndDisposeIfUnused(this);
      resultsB.handle().releaseAndDisposeIfUnused(this);
    }

    public Progress getProgress() {
      return progress;
    }
  }

  /**
   * A {@link QueryResponse} that is the union of other {@link QueryResponse}.
   */
  public static class UnionQueryResponse implements QueryResponse, Resource.User {

    private final Handle handle = new Handle(this);

    // keep these around so they can be disposed
    private final QueryResponse resultsA;
    private final QueryResponse resultsB;
    private final CompositeList<Line> compositeList;
    private final UniqueList<Line> union;
    private final Progress progress = new Progress();
    private final ResultsPopulator resultsPopulator;

    private final String shortDescription;

    public UnionQueryResponse(QueryResponse resultsA, QueryResponse resultsB) {
      this.resultsA = resultsA;
      this.resultsB = resultsB;

      // register myself user for resultsA and resultsB
      resultsA.handle().reserve(this);
      resultsB.handle().reserve(this);

      // union the results
      EventList<Line> aLines = resultsA.getLines();
      EventList<Line> bLines = resultsB.getLines();
      compositeList = new CompositeList<Line>(aLines.getPublisher(), aLines.getReadWriteLock());
      compositeList.addMemberList(aLines);
      compositeList.addMemberList(bLines);
      union = new UniqueList<Line>(compositeList, lineComparator);

      shortDescription = resultsA.getShortDescription() + "+" + resultsB.getShortDescription();

      resultsPopulator = new CombinedResultsPopulator(
          Arrays.asList(resultsA.getResultsPopulator(), resultsB.getResultsPopulator()));
    }

    public EventList<Line> getLines() {
      return union;
    }

    public String getShortDescription() {
      return shortDescription;
    }

    public ResultsPopulator getResultsPopulator() {
      return resultsPopulator;
    }

    public Handle handle() {
      return handle;
    }

    public void dispose() {
      union.dispose();
      compositeList.dispose();
      resultsA.handle().releaseAndDisposeIfUnused(this);
      resultsB.handle().releaseAndDisposeIfUnused(this);
    }

    public Handle getHandle() {
      return handle;
    }

    public Progress getProgress() {
      return progress;
    }
  }

  /**
   * Composite of multiple ResultsPopulators.
   */
  public class CombinedResultsPopulator implements ResultsPopulator {
    List<ResultsPopulator> delegates;

    public CombinedResultsPopulator(List<ResultsPopulator> delegates) {
      this.delegates = new ArrayList<ResultsPopulator>(delegates);
    }

    public State getState() {
      State state = State.ALL_RESULTS_DOWNLOADED;
      for (ResultsPopulator delegate : delegates) {
        state = state.union(delegate.getState());
      }
      return state;
    }

    public void fetchMoreResults() {
      for (ResultsPopulator delegate : delegates) {
        if (delegate.getState() == State.RESULTS_OUTSTANDING) {
          delegate.fetchMoreResults();
        }
      }
    }
  }
}
